[TOC]
# Hive
> 起源自:FaceBook
- Hive提供SQL的开发接口,用户可以直接使用SQL来操作Hadoop
- Hive本身只是一个翻译的角色,底层分布式存储和分布式计算都是靠Hadoop来实现的
- 高度依赖于Hadoop
## 本质
一种特殊的支持SQL开发接口的Hadoop客户端
## 功能
- **将文件映射成表的数据**[工作中主要使用此功能构建数仓]
- Hive的存储:HDFS
- 功能二:将SQL语句转换为MapReduce程序,提交给yarn运行[工作中使用较少,替代品:Impala、SparkSQL
- MapReduce是对文件进行操作
- SQL是对表进行操作
- Hive是对表处理的,底层的MapReduce是对文件进行处理的
## 应用场景
应用于构建**数据仓库**
## 架构
- 客户端:用于提供与用户交互的界面,实现SQL开发
- 服务端:
- 负责分析SQL,读写元数据,提交程序给Hadoop
- 连接器:负责维护与客户端的连接
- 解析器:负责解析SQL语句构建语法树
- 判断数据库、表是否存在
- 语法是否正确
- 最终得到一个逻辑计划
- 优化器:优化这个逻辑,得到物理计划
- 执行器:执行物理计划得到结果返回给客户端
- 元数据:存储Hive中关键性信息
- Hive中所有数据库、所有表的信息
- HDFS与Hive表的映射关系
- Hadoop:Hive所有的请求都是给Hadoop**实现**的
- Hive自己不是分布式的
- Hive能实现分布式存储和分布式计算
- 底层:
- 存储:HDFS
- 计算:MapReduce+yarn
## 常用配置
- 本地模式
~~~shell
set hive.exec.mode.local.auto=true;
~~~
- 本地模式的三个条件:
- 1.job的输入数据大小必须小于参数:hive.exec.mode.local.auto.inputbytes.max(默认128MB)
- 2.job的map数必须小于参数:hive.exec.mode.local.auto.tasks.max(默认4)
- 3.job的reduce数必须为0或者1
## 元数据服务
- 存储内容:**Hive中关键性数据,数据库、表、列的信息**
- 存储位置
- 默认位置:derby数据库
- Hive自带的文本型数据库,轻量级的数据库
- 一般用于嵌入式系统中的数据存储
- 不方便管理和维护,不方便共享
- 自定义位置:MySQL
- 官方推荐使用的存储方式
- 工作中使用的方式(**几乎所有的元数据都存放在MySQL**)
- 元数据的**访问**方式:**内嵌模式**、**本地模式**、**远程模式**
- 内嵌模式:元数据使用默认存储,直接访问derby
- 本地模式:元数据使用RDBMS(关系型数据库管理系统):MySQL,Hive
- RDBMS:关系型数据库管理系统
- NoSQL:非关系型数据库
- 远程模式:元数据存储是使用MySQL,Hive服务端访问Metastore服务来访问元数据 (metastroe相当于一个中介)
### 元数据共享
- **问**:使用Spark/Impala/presto等对Hive中的数据进行计算从而代替hive底层的MapReduce计算,如何能让Spark等工具读取到Hive中的表,以及对应的HDFS的数据呢?
- **解决**:所有的数据都存储在元数据中,只要**将Hive元数据共享刚给其他工具即可**
> 默认的,Hive服务端会将Hive客户端的操作请求翻译成MapReduce API并提交给Hadoop,实现对Hive中的数据进行计算,此时Hive仅仅充当一个翻译工具,目的是将用户指定SQL语言翻译成MapReduce代码,而MapReduce运行计算是非常慢的,这样的方式效率低下,而Spark等工具效率高速度快,可以使用spark等工具代替Hive底层的MapReduce实现计算。但是这样一来,spark等工具怎么知道Hive中的表在哪里以及对应的HDFS数据在哪里呢,为了解决此问题,我们知道Hive的元数据中存储了Hive中的关键性信息,如数据库、表、列的信息,只要将Hive中的元数据共享给其他工具即可。
- 问:如何实现共享问题?
- 解决:**构建元数据管理服务MetaStore**,让所有需要访问Hive中表和对应的HDFS数据的工具直接访问MetaStore,MetaStore来告诉他们对应的数据在哪。
> 如果让spark等其他计算工具直接访问Hive的元数据,会产生一系列权限问题,Hive的元数据是采用MySQL存储的,MySQL会对客户端的访问进行权限检查,使得访问不通过。但即使没有权限检查,其他工具直接访问Hive的元数据,也不清楚不知道访问到的元数据是干嘛的,有怎样的信息,为了解决此问题,Hive专门构建了MetaStore,让其他工具直接将**元数据读写请求**发送至MetaStore,MetaStore会解析客户端的请求,并告诉其需要访问的的数据在哪,以及数据的具体信息。
### 元数据管理服务
- MetaStore:为了实现元数据共享而涉及的一种专有的元数据管理服务
- 元数据管理服务的开启由配置决定,在hive-site.xml中:
~~~xml
hive.metastore.uris
thrift://node3:9083
~~~
- 配置了这个服务,就必须先开启MetaStore这个服务再使用Hive
> 由于所有对Hive元数据的读写请求都是经过MetaStore来处理的,所以必须开启MetaStore服务才能使得Hive客户端访问Hive元数据。举个栗子:早期打电话,会有电话中转,张铁妞先将电话拨到服务台,告诉接线员我要打给王大锤,于是接线员就将线路接到了王大锤家,如果MetaStore没有先开启,张铁妞就不能直接拨打王大锤家的电话。
## 表的分类与结构
### 管理表
> MANAGED_TABLE
- Hive中默认创建的表的类型
- 特点:
- 只要不手动删除,这张表就一直存在
- 手动删除管理表:元数据会被删除,数据也会被删除
### 临时表
> TRMPORARY
- 特点:
- 这张表创建的客户端一旦断开连接,临时表会自动删除
- 一般用于存储临时数据,并且表用完以后不会再被使用
### 外部表
> EXTERNAL_TABLE
- 特点:
- 手动删除外部表:**元数据会被删除,但是数据不会被删除**
> 某个用户在读取该表之后将其删除,只是删除了元数据,数据仍然保留在HDFS上,多个人对同一份数据进行读取并建立了外部表,每个人使用完之后删除了自己的表,不影响最终保留在HDFS上的那份数据。
~~~SQL
CREATE EXTERNAL TABLE TABLE_NAME(
COL1 string,
COL2 string,
...
)
PARTITION BY (daystr string)
LOCATION '/user/hive/warehouse/some_place'
~~~
- 应用:
- 如果这份数据比较重要,建立外部表保证数据安全
- 入股多个人需要使用这张表读取同一份数据,任何一个表被删除,不能影响数据
### 结构
#### 普通结构表
- 普通结构表和HDFS文件之间的映射关系
- Hive表的最后一级目录就是表的目录
- 表中的数据按照原始数据文件形式存在
> 当使用load data语句将文件与Hive中的表关联后,无论原先HDFS文件存储在HDFS上的哪个位置,都会被移送到/user/hive/warehouse/数据库名/表名这个目录下,并且仍然按照文件形式存储
#### 分区结构表
> 降低程序的负载,提高程序的效率
**设计思想**是**优化底层MapReduce的输入,根据分区直接对数据进行过滤,避免不需要用到的数据进入程序。**
> 将数据**按照目录拆分**,**不同分区就是不同的目录**,在这种情况下,**如果过滤条件不是分区字段,那么分区优化是无效的**
为什么这么说?
- 有个场景:
- 在表的目录下(hivelog)存储了多个日志文件,并且每个日志文件以时间命名(如:2020-08-29),如果需要对8月29日的日志文件进行处理,则需要过滤:
~~~SQL
select count(*) from hivelog where daystr=2020-08-29;
~~~
> 这么简单的依据SQL,我们知道Hive的底层是通过MapReduce来实现的,所以在底层MapReduce读取了HDFS上hivelog这个目录,将这个目录下的所有文件作为程序的输入,过滤的目的可以达到,可是这么一来,就需要读取所有的文件,而MapReduce运行起来耗费时间和资源
- 另一个场景:
- 同样在hivelog下存储了多个文件,不过与上个场景不同的是,每天日志文件都被上一级以时间命名的目录包裹起来,如:
~~~
/user/hive/warehouse/practice.db/2020-08-28/2020-08-28.log
/user/hive/warehouse/practice.db/2020-08-29/2020-08-29.log
/user/hive/warehouse/practice.db/2020-08-30/2020-08-30.log
~~~
> 这样,如果我们想要过滤读取到2020-08-29这一天的日志再次执行
>
> ~~~SQL
> select count(*) from hivelog where daystr=2020-08-29;
> ~~~
>
> 在底层MapReduce程序的输入是不一样的,它只读取了2020-08-29.log这个文件,文件读取量是上一种场景的三分之一
- 应用场景:
- 需要按照一定的时间维度进行数据处理,数据量非常大
- 实现方式:
- 方式一:**手动分区**(静态分区)
- 应用场景:数据本身就是**已经按照分区规则分好**了的
- 例如hive的日志就是按照天日期分好的
- 这样就可以之间创建一张分区表,将对应的文件按照分区条件加载到不同对的分区中
> 加载,这个时候,Hive实际在HDFS中数据表的目录下创建了N个以分区条件命名的目录
~~~SQL
load data local inpath '/export/datas/emp10.txt' into table tb_emp_part1 partition(department = 10);
此时目录名就是department=10
~~~
- 例如
~~~sql
insert overwrite table demo_static_partition partition(year="2020", month="04", day="2020-04-10", hour="22")
select
user_id,
user_name,
trade_year as year ,
trade_month as month,
trade_day as day,
trade_hour as hour
from user_demo
where
trade_year="2020"
and trade_month="04"
and trade_day="2020-04-10"
and trade_hour="22"
~~~
- 分区表的分区过滤,直接通过元数据找到分区对应的HDFS位置作为MapReduce的输入
- 方式二:**自动分区**(动态分区)
- 应用场景:**数据本身没有做分区**,拆分不同的文件
- 例如:Nginx的日志每天都追加写入同一个文件中
- 实现步骤:
1. 开启自动分区(开启非严格模式)
~~~SQL
set hive.exec.dynamic.partition.mode=nonstrict;
~~~
2. 创建一张分区表,将待分区的文件加载到这张普通表中
3. 再创建一张分区结构表,使用partitioned by字段指定分区的字段条件
4. 从普通表中查询将数据分区写入创建的分区结构表中
~~~SQL
insert into table 分区表 partition(分区字段) select * from 普通表;
~~~
~~~sql
insert overwrite table demo_dynamic_partition partition(year=year, month=month, day=day, hour=hour)
select
user_id,
user_name,
trade_year as year ,
trade_month as month,
trade_day as day,
trade_hour as hour
from user_demo
~~~
- 使用动态分区与静态分区的注意事项和区别
- 区别:
- 动态分区,在运行时根据列的取值去自动创建分区,有多少种值就多少个分区,会为每个分区分配reduce个数,当分区量过多时,reduce也会增加
- 静态分区不管分区有没有数据都会创建该分区,而动态分区则会有结果就创建,没结果就不会创建
- 动态分区根据字段的变化而变化,手动分区是文件已经按照字段分区规则分好,手动指定分区的值为静态值。
- 注意事项:
- 需要开启属性配置:
~~~sql
-- Hive默认配置值
-- 开启或关闭动态分区
hive.exec.dynamic.partition=false;
-- 设置为nonstrict模式,让所有分区都动态配置,否则至少需要指定一个分区值
hive.exec.dynamic.partition.mode=strict;
-- 能被mapper或reducer创建的最大动态分区数,超出而报错
hive.exec.max.dynamic.partitions.pernode=100;
-- 一条带有动态分区SQL语句所能创建的最大动态分区总数,超过则报错
hive.exec.max.dynamic.partitions=1000;
-- 全局能被创建文件数目的最大值,通过Hadoop计数器跟踪,若超过则报错
hive.exec.max.created.files=100000;
-- 根据个人需要配置
set hive.exec.dynamic.partition=true;
set hive.exec.dynamic.partition.mode=nonstrict;
set hive.exec.max.dynamic.partitions.pernode=1000;
set hive.exec.max.dynamic.partitions=10000;
set hive.exec.max.created.files=1000000;
~~~
- 混合使用时,静态分区必须在动态分区的前面
~~~sql
insert overwrite table demo_static_partition
partition(year="2020", month="04",
day=day, hour=hour)
select user_id, user_name,
trade_year as year ,
trade_month as month,
trade_day as day,
trade_hour as hour
from user_demo
where trade_year="2020"
and trade_month="04"
~~~
- 多级分区
- 比如一个需求:按照天分区,再按照小时分区
- 这个文件是按照时间分区好的,因此需要执行手动分区
- 在创建分区结构表时使用语句
~~~SQL
partitioned by(daystr string, hourstr string)
~~~
- 将HDFS上或本地的文件加载到分区结构表中,指定分区
~~~SQL
partition (daystr='20150828',hourstr='18');
partition (daystr='20150828',hourstr='19');
~~~
- 特点:
- 分区是目录级的
- 分区字段是逻辑存在,并不是物理存在的,实际的文件中并没有这个字段
- Hive分区是将数据文件按照分类存储在不同的目录中,优化输入
#### 分桶结构表
> 实际使用中,除了为了用来**专门优化join减少比较的次数**,其他一无是处。
- 本质:**就是底层MapReduce的分区**(多个reduce下,在map端shuffle阶段为行数据打上标签用来标记被哪一个reduce处理)
- 规则:
- 桶的个数:底层MapReduce中Reduce的个数
- clustered by :按照哪一列作为Map输出的Key,进行分区
- 按照Key的哈希取余
- 注意:对于分桶表,不能使用load data的方式进行数据插入操作,因为load data导入的数据不会有分桶结构。
> 如何避免针对桶表使用load data插入数据的误操作呢?
>
> 限制对桶表进行load操作 set hive.strict.checks.bucketing = true;
>
> 也可以在CM的hive配置项中修改此配置,当针对桶表执行load data操作时会报错。
- 如何将数据装载进入桶表呢?
- 先创建临时表,通过load data将txt文本导入临时表。
~~~sql
--创建临时表
create table temp_buck(id int, name string)
row format delimited fields terminated by '\t';
--导入数据
load data local inpath '/tools/test_buck.txt' into table temp_buck;
~~~
- 使用insert select语句间接的把数据从临时表导入到分桶表。
~~~mysql
--启用桶表
set hive.enforce.bucketing=true;
--限制对桶表进行load操作
set hive.strict.checks.bucketing = true;
--insert select
insert into table test_buck select id, name from temp_buck;
--分桶成功
~~~
> **注意**,hive使用对分桶所用的值进行hash,并用hash结果除以桶的个数做取余运算的方式来分桶,保证了每个桶中都有数据,但每个桶中的数据条数不一定相等。
>
> 如果另外一个表也按照同样的规则分成了一个个小文件。两个表join的时候,就不必要扫描整个表,只需要匹配相同分桶的数据即可,从而提升效率。
>
> 在数据量足够大的情况下,分桶比分区有更高的查询效率。
#### 分区和分桶的区别
1. 分桶和分区两者不干扰,可以把分区表进一步分桶;
2. 分桶对数据的处理比分区更加细粒度化:分区针对的是数据的存储路径;分桶针对的是数据文件;
3. 分桶是按照列的哈希函数进行分割的,相对比较平均;而分区是按照列的值来进行分割的,容易造成数据倾斜。
4. 分区表按照目录来拆分,不同分区就是不同的目录,而分桶表按照文件进行拆分,按照某一列的Hash值取余来装入不同的桶,桶的个数就是底层Reducer的个数
## Join与排序
### Join
- 内连接
- 左连接
- 右连接
- 全连接
> 注意:**严禁产生笛卡尔积**,大数据环境中的数据量巨大,而笛卡尔积会产生更大的数据量。
注意规避一下几种写法:
~~~MYSQL
# 产生笛卡尔积
select a.*, b.*
from a,b;
# 未指定join条件,产生笛卡尔积
select a.*, b.*
from a join b;
# 不规范的join写法,使用where会在全部的数据中过滤从而得到指定条件的数据,因此是在笛卡尔积产生的条件下按照条件过滤得到想要的数据
select a.*, b.*
from a join b where 条件;
~~~
- 底层实现:
- reduce join:join过程发生在Reduce端
- 特点:
- 必须经过shuffle,通过shuffle将关联的字段分组,在reduce端进行关联
- 适合于大表join大表
- Map join:底层发生在map端,不经过shuffle
- 特点:
- 将小数据放入每台机器的内存中,所有的join都发生在内存中
- Hive会优先调用map join,如果map join条件不能满足,会自动调用Reduce join(这由配置文件决定hive.auto.convert.join)
- 适合小数据join大数据
- SMB Join = Map Join + Bucket Join
- 两张表都是桶表
- B表桶的个数必须等于A表的桶的个数
- join的字段 = 分桶的字段 = 排序的字段
### 排序
- order by:全局有序,只能有一个reduce
- sort by:局部有序,每个Reduce Task内部有序(如果只有一个reduce,其效果和order by效果一样)
- distribute by:干预底层MapReduce的分区,指定按照哪一列作为key进行分区
- clustered by:当distribute by 和sort by指定的字段是同一个字段时,可以直接使用clustered by
## 复杂数据类型
### array类型
~~~mysql
row format delimited fields terminated by '\t' --指定文件中列的分隔符
COLLECTION ITEMS TERMINATED BY ','; --指定数组中每个元素的分隔符
~~~
### Map类型
~~~mysql
row format delimited fields terminated by ',' --指定文件中列的分隔符
COLLECTION ITEMS TERMINATED BY '-' --指定每个KeyValue之间的分隔符
MAP KEYS TERMINATED BY ':'; --指定KEY和Value之间的分隔符
~~~
## 函数
### 内置函数
- 列举:
~~~mysql
show functions;
~~~
- 查看用法:
~~~mysql
desc function func_name;
~~~
- 查看函数和示例:
~~~mysql
desc function extended func_name;
~~~
### 自定义函数
#### 分类
- UDF:一对一,普通
- UDAF:多对一,聚合
- UDTF:一对多
- 比如explode
#### 开发使用
- 开发一个UDF:
- 开发一个类继承自UDF类
- 实现一个或者多个evaluate方法
- 在evaluate方法中实现数据的处理逻辑
- 将结果作为返回值返回
- 将自己写的类打成jar包,添加到Hive的环境变量中
- 本地编写类,打成jar包
- 上传至Linux环境
- 进入Hive(beeline)
~~~mysql
add jar /export/datas/udf.jar
~~~
- 在Hive中创建一个函数
~~~mysql
create temporary function transDate as
'bigdata.iroohom.me.hive.udf.UserUDF';
~~~
- 使用自己开发的函数
~~~mysql
select transDate("21/Sep/2019:13:30:00");
~~~
- UDTF的使用:一对多
- 原始数据是一行一列
- 需求结果是:多行多列
- UDAF:多对一,聚合
### 侧视图
> lateral view
- 功能:专门用于搭配UDTF使用,将UDTF与其他字段进行拼接
- 什么是视图?
- 关键字:view
- 语法:
~~~mysql
create view | table
~~~
- 定义:是一种只读表
- 使用:当做表来使用,不过不能修改
- 设计:将UDTF的结果构建成一个类似于视图的形式,与原表进行拼接
- 使用:
- 语法:
~~~mysql
select …… from tabelA lateral view UDTF(xxx) 视图名 as a,b,c
~~~
- 数据:
~~~mysql
http://facebook.com/path/p1.php?query=1
域名 路径 参数
facebook.com /path/p1.php query=1
~~~
- 示例:
~~~mysql
select
a.id,
b.host,
b.path
from
tb_url a
lateral view parse_url_tuple(url, 'HOST',"PATH") b as host,path;
~~~